# -*- coding:utf-8 -*-
# @Time :2021/11/4 21:00
# @Author : Martin

import logging
import os
import re
from time import sleep
from typing import List
import time

from selenium import webdriver
from selenium.common.exceptions import ElementClickInterceptedException, TimeoutException, ElementNotVisibleException, \
    InvalidSelectorException, UnexpectedAlertPresentException
from selenium.webdriver import ActionChains
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support.select import Select

from web_keys.web_wait import WebDriverWaitExtend

# def handler_alert(func):
#     @wraps(func)
#     def wrp_func()
from conf.config import RunConfig

class BasePage:
    _host = RunConfig.url
    _base_url = None
    def __init__(self, driver: WebDriver = None, logger: logging.RootLogger = None):

        self.driver = driver
        # 定义日志器
        if logger:
            self.logger = logger
        else:
            self.logger = logging.getLogger(__name__)

        # 判断当前页面和待打开的页面不一致时，重新打开页面
        if self._base_url:
            page_url = self._host + self._base_url
            if page_url != self.driver.current_url:
                self.open(page_url)

        self.work_path = os.path.dirname(os.path.dirname(__file__))

    # 访问url
    def open(self, url):
        """
        打开网页
        :param url:
        :return:
        """
        self.logger.info("打开网址<{}>".format(url))
        self.driver.get(url)


    def quit(self):
        self.logger.info("退出浏览器")
        self.driver.quit()

    def close(self, w=None):
        self.logger.info("关闭当前窗口")
        if w:
            self.driver.switch_to.window(w)
        # else:
        #     self.driver.switch_to.window(self.driver.current_window_handle)
        self.driver.close()

    def locate(self, by, value, rel_by=None, rel_value=None, direction=None) -> WebElement:
        """
        单个元素定位
        :param by: 定位元素方式by
        :param value: 定位元素值
        :param rel_by: 相对定位,暂无用
        :param rel_value: 相对定位,暂无用
        :param direction: 相对定位,暂无用
        :return: 元素对象
        """
        # self.logger.info(f'开始定位元素<{by}={value}>')
        try:
            return self.web_el_wait(by, value)
        except TimeoutException as e:
            self.logger.exception(f"元素{by}={value}定位超时,异常信息={e}")
            raise e
        except ElementNotVisibleException as e:
            self.logger.exception(f"元素{by}={value}元素不可见,异常信息={e}")
            raise e
        except InvalidSelectorException as e:
            self.logger.exception(f"元素{by}={value}定位符不合法")
            raise e
        except UnexpectedAlertPresentException as e:
            self.driver.switch_to.alert.accept()
            self.logger.exception(f"存在不被预期的alert弹框")
            # raise e

    # def locate(self, by, value, rel_by=None, rel_value=None, direction=None):
    #     """
    #     相对定位器，需要selenium4.0
    #     :param by: 目标元素定位方式
    #     :param value: 目标元素定位值
    #     :param rel_by: 相对元素定位方式
    #     :param rel_value: 相对元素定位值
    #     :param direction: 相对方向 left right above below near
    #     :return: WebElement
    #     """
    #     try:
    #         if direction is None:
    #             return self.web_el_wait(by, value)
    #         else:
    #             # 获取相对元素
    #             rel_ele = self.web_el_wait(rel_by, rel_value)
    #             direction_dict = {
    #                 'left': 'to_left_of',  # 左侧
    #                 'right': 'to_right_of',  # 右侧
    #                 'above': 'above',  # 上方
    #                 'below': 'below',  # 下方
    #                 'near': 'near'  # 靠近
    #             }
    #             # 获取相对方向方法名称
    #             rel_method = direction_dict[direction]
    #             # 利用反射获取元素对象
    #             return self.web_el_wait(getattr(locate_with(by, value), rel_method)(rel_ele))
    #     except Exception as e:
    #         self.logger.exception("元素定位异常,异常信息={}".format(e))
    #         raise e

    def locates(self, by, value) -> List[WebElement]:
        """
        批量元素定位
        :return:
        """
        try:
            return self.web_els_wait(by, value)
        except TimeoutException as e:
            self.logger.exception(f"元素{by}={value}定位异常,异常信息={e}")
            raise e

    # 点击操作
    def click(self, by, value, rel_name=None, rel_value=None, direction=None):
        """
        控件点击
        :param by:
        :param value:
        :param rel_name:
        :param rel_value:
        :param direction:
        :return:
        """
        self.logger.info("点击控件{}={}".format(by, value))
        ele = self.locate(by, value, rel_name, rel_value, direction)
        try:

            self.click_el(ele)
            # self.wait(1)
        except ElementClickInterceptedException as ece:
            self.logger.exception(f"ElementClickInterceptedException：元素{by}={value}不可点击，异常信息{ece}")
            raise ece
        except ElementNotVisibleException as e:
            self.logger.exception(f"ElementNotVisibleException:元素{by}={value}不可见，异常信息{e}")
            raise e

    def click_el(self, el: WebElement):
        try:
            if el:
                el.click()
            else:
                pass
        except:
            self.scroll_ele(el, 'center')
            el.click()

    # 输入
    def input(self, by, value, txt, rel_by=None, rel_value=None, direction=None):
        """
        文本框输入
        :param by:
        :param value:
        :param txt:
        :param rel_by:
        :param rel_value:
        :param direction:
        :return:
        """
        self.logger.info(f"在控件{by}={value}输入文本<{txt}>")
        input_ele = self.locate(by, value, rel_by, rel_value, direction)
        self.input_ele(input_ele, txt)

    def input_ele(self, ele, txt):
        try:
            ele.clear()
            ele.send_keys(txt)
        except ElementNotVisibleException:
            self.scroll_ele(ele)
            ele.clear()
            ele.send_keys(txt)

    def remove_readonly(self, by, value):
        ele = self.locate(by,value)
        self.logger.warning('去除只读属性')
        self.excute_js('arguments[0].removeAttribute(\"readonly\")', ele)
    # 上传
    def upload(self, by, value, txt, rel_by=None, rel_value=None, direction=None,event=None):
        """
        标准input上传控件
        :param by:
        :param value:
        :param txt:
        :param rel_by:
        :param rel_value:
        :param direction:
        :return:
        """
        root_path = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
        self.logger.info("上传文件{}".format(root_path + txt))
        input_ele = self.locate(by, value, rel_by, rel_value, direction)
        input_ele.clear()
        input_ele.send_keys(root_path + txt)
        # if event:
        #     self.excute_js(f"arguments[0].{event}()", input_ele)

    # 显式等待
    def web_el_wait(self, by, value=None) -> WebElement:
        """
        显示等待
        :param by:
        :param value:
        :return:
        """
        return WebDriverWaitExtend(self.driver, 10, 0.5).until(
            lambda el: self.driver.find_element(by, value), message=f'元素{by}={value}查找失败')

    def web_els_wait(self, by, value=None) -> List[WebElement]:
        """
        批量查询元素显示等待
        :param by:
        :param value:
        :return:
        """
        return WebDriverWaitExtend(self.driver, 10, 0.5).until(
            lambda el: self.driver.find_elements(by, value), message=f'元素{by}={value}查找失败')

    # 强制等待
    def wait(self, time):
        self.logger.info(f'强制等待{time}秒')
        sleep(int(time))

    def switch_frame(self, txt):
        """
        frame切换
        :param txt: 可以是id，name，webelement
        :return:
        """
        self.logger.info(f'切换至iframe框，{txt}')
        # txt可以是id或者name属性的值，也可以是元素,源码已经做了自动识别
        self.driver.switch_to.frame(txt)

    # 切换default窗体
    def switch_default(self):
        """
        切换默认窗体
        :return:
        """
        self.driver.switch_to.default_content()

    def assert_ele_text(self, by, value, txt, rel_by=None, rel_value=None, direction=None):
        """
        校验元素文本
        :return: 成功返回True,失败返回错误信息
        """
        reality = self.locate(by, value, rel_by, rel_value, direction).text
        assert txt == reality, '断言失败，实际结果为：{}'.format(reality)

    @staticmethod
    def assert_text_by_re(string, pattern):
        if re.match(pattern, string, flags=0):
            return True
        else:
            return False

    def move_to_ele(self, by, value, rel_by=None, rel_value=None, direction=None):
        """
        鼠标移动操作
        """
        self.logger.info(f'鼠标移动至元素<{by}>=<{value}>')
        ele = self.locate(by, value, rel_by, rel_value, direction)
        action = ActionChains(self.driver)
        action.move_to_element(ele)
        action.perform()

    def excute_js(self, js, *args):
        """调用js"""
        try:
            self.logger.info(f'执行js脚本<{js}>，参数为<{args}>')
            return self.driver.execute_script(js, *args)
        except BaseException:
            raise BaseException

    # def js_locate(self, by, value):
    #     if by == 'id':
    #         js = f"return document.getElementById('{value}')"
    #     # elif by == 'name':
    #     #     js = f"return document.getElementsByName('{value}')"
    #     elif by == 'css selector':
    #         js = f"return document.querySelector('{value}')"
    #     else:
    #         raise ValueError(f"by={by}不支持，当前支持id和css selector")
    #     return self.excute_js(js)

    def js_input(self, locate, txt, event=None):
        """
        var ev = new Event('input', { bubbles: true});
        ev.simulated = true;
        element.value = 'Something new';
        element.dispatchEvent(ev);
        """
        if isinstance(locate, str):
            el = self.excute_js(f"return document.querySelector('{locate}')")
        elif isinstance(locate, WebElement):
            el = locate
        self.add_attribute(el, 'value', txt)
        # 触发输入生效事件
        if event:
            self.excute_js(f"arguments[0].{event}()", el)
        #
        # self.driver.execute_script("arguments[0].setAttribute(arguments[1],arguments[2])", ele, name, value)


    def js_click(self, locate):
        """
        js点击控件，必须是css定位符
        :param locate:
        :return:
        """
        self.logger.info('执行js点击')
        el = self.excute_js(f"return document.querySelector('{locate}')")
        self.excute_js("arguments[0].click()", el)

    def set_attribute(self, ele, name, value):
        """
        封装设置页面对象的属性值的方法
        调用JS代码修改页面元素的属性值，arguments[0]~arguments[1]分别
        :param ele: 元素对象
        :param attributeName: 属性值
        :param value: 需要设置的值
        """
        self.logger.info(f'js设置元素属性<{name}>的值等于<{value}>')
        self.driver.execute_script("arguments[0].setAttribute(arguments[1],arguments[2])", ele, name, value)

    def add_attribute(self, ele, name, value):
        """
        封装向页面标签添加新属性的方法
        调用JS给页面标签添加新属性，arguments[0]~arguments[2]分别
        会用后面的element，attributeName和value参数进行替换
        添加新属性的JS代码语法为：element.attributeName=value
        比如input.name='test'
        :param ele:
        :param name:
        :param value:
        :return:
        """
        self.logger.info(f'js添加元素属性<{name}>,值等于<{value}>')
        self.driver.execute_script("arguments[0].%s=arguments[1]" % name, ele, value)

    def get_elements_txt(self, by, value):
        """
        批量获取元素文本
        :return: 文本列表
        """
        elements = self.locates(by, value)
        texts = [ele.text for ele in elements if ele.text != ""]
        self.logger.info(f"批量获取文本信息<{texts}>")
        return texts

    def get_element_txt(self, by, value):
        """
        批量获取元素文本
        :return: 文本列表
        """
        element = self.locate(by, value)
        txt = element.text
        self.logger.info(f"获取控件<{by}>=<{value}>的文本信息为<{txt}>'")
        return txt

    def get_element_attr(self, by, value, attr):
        """
        批量获取元素文本
        :return: 文本列表
        """
        element = self.locate(by, value)
        txt = element.get_attribute(attr)
        self.logger.info(f"获取控件<{by}>=<{value}>的{attr}属性值为<{txt}>'")
        return txt

    def single_checked(self, by, value):
        """
        复选框勾选
        :param by:
        :param value:
        :return:
        """
        checked_ele = self.locate(by, value)
        if not checked_ele.is_selected():
            self.logger.info(f'执行-勾选元素<{by}={value}>')
            # checked_ele.click()
            self.click_el(checked_ele)
        else:
            self.logger.warning(f'元素<{by}={value}>当前已勾选')

    def single_unchecked(self, by, value):
        """
        复选框取消勾选
        :param by:
        :param value:
        :return:
        """
        checked_ele = self.locate(by, value)
        if checked_ele.is_selected():
            self.logger.info(f'执行-勾选元素<{by}={value}>')
            checked_ele.click()
        else:
            self.logger.warning(f'元素<{by}={value}>当前未勾选')

    def scroll_single_checked(self, parent, item):
        """
        div滚动框滚动选择
        :param parent:
        :param item:
        :return:
        """

        # parent_height = checked_ele.find_element_by_xpath('..').size['height']
        # js='document.getElementsByClassName("J-article J-scroll-box")[0].scrollTop=10000'
        # js='document.getElementsById("J-article J-scroll-box").scrollTop=10000'
        # s1

        # document.querySelector('.item3').offsetTop
        self.logger.info('滚动元素并点击')
        parent_ele = self.locate('css selector', parent)
        # self.excute_js("arguments[0].scrollIntoView(false);", parent_ele)
        self.scroll_ele(parent_ele)
        checked_ele = self.locate('css selector', item)
        if checked_ele.is_enabled():
            checked_ele.click()
        else:
            # js = f'document.querySelector("{parent}").scrollTop = 1000'
            js = f"document.querySelector('{parent}').scrollTop = document.querySelector('{item}').offsetTop"
            self.logger.info(f"js滚动父元素{parent}中的子元素{item}")
            self.excute_js(js)
            checked_ele.click()

    def scroll_ele(self, el, top="center"):
        """
        js滚动元素
        :param el: 元素对象
        :param top: true顶部对齐 false:底部对齐 center 居中
        :return:
        """
        if top == "center":
            param = '{block: "center", inline: "center"}'
            self.logger.info('滚动元素至屏幕中央')
        elif top == 'up':
            param = 'true'
            self.logger.info('滚动元素至屏幕顶部')
        elif top == 'down':
            param = 'false'
            self.logger.info('滚动元素至屏幕底部')
        else:
            raise ValueError('top参数值错误，只能是center，true，false')
        self.excute_js(f"arguments[0].scrollIntoView({param});", el)

    #
    def multi_checked(self, by, value):
        """
        单个复选框点击,根据复选框当前勾选状态和预期操作状态比对,不等则点击,否则不操作
        :param by:
        :param value:
        :param txt: 预期勾选状态
        :return:
        """
        ele_list = self.locates(by, value)
        for ele in ele_list:
            # status = ele.get_attribute('checked')
            # if status != txt:
            item = ele.get_attribute('value')
            self.logger.info(f'选中元素value={item}')
            ele.click()

    def select(self, by, value, param, type_="value"):
        """

        :param by: 定位方式
        :param value: select框定位
        :param param: 选项值
        :param type_: value(根据value值勾选)，text(根据可见文本勾选)
        :return:
        """
        # document.querySelector('#divisionCodeTd > div').click();
        self.logger.info(f'定位下拉框<{by}={value}>')
        opt = self.locate(by, value)
        sel = Select(opt)
        if type_ == "text":
            self.logger.info(f"选择选项文本<{param}>")
            sel.select_by_visible_text(param)
        elif type_ == "value":
            self.logger.info(f"选择选项值<{param}>")
            sel.select_by_value(param)

    def switch_alert(self, param="accept"):
        self.logger.info('开始处理alert弹框')
        self.wait(3)

        alert = self.driver.switch_to.alert
        self.logger.info('切换alert弹框成功')
        msg = alert.text
        self.logger.info(f'alert弹框提示文本<{msg}>')
        if param == "accept":
            self.logger.info('点击弹框确认')
            alert.accept()
        elif param == "dismiss":
            self.logger.info('点击弹框取消')
            alert.dismiss()
        return msg

    def switch_current_handle(self):
        handles = self.driver.window_handles
        # handle_num = len(handles)
        # if handle_num > 1:
        #     # for i in range(handle_num-1):
        #     #     self.driver.close()
        self.driver.switch_to.window(handles[-1])
        self.logger.info(f'切换到窗口<{self.driver.title}>')

    def switch_handle(self, txt):
        self.logger.info(f'切换到窗口<{txt}>')
        self.driver.switch_to.window(txt)
        self.logger.info(f'切换到窗口<{self.driver.title}>')

    def is_exist(self, by, value):
        try:
            return self.locate(by, value)
        except:
            return False

    def scroll_bottom(self):
        self.logger.info('滑动至屏幕底部')
        height = self.get_page_height()
        self.excute_js(f'window.scrollTo(0,{height})')

    def scroll_top(self):
        self.logger.info('滑动至屏幕顶部')
        self.excute_js('window.scrollTo(0,0)')

    def scroll_center(self):
        self.logger.info('滑动至屏幕中部')
        height = self.get_screen_height()
        width = self.get_screen_width()
        half_height = round(int(height)/2)
        half_width = round(int(width) / 2)
        self.excute_js(f'window.scrollBy({half_width},{half_height})')

    def get_screen_width(self):
        return self.excute_js("return window.screen.width")

    def get_screen_height(self):
        return self.excute_js("return window.screen.height")

    def get_page_height(self):
        return self.excute_js("return document.body.scrollHeight")


    def screen_shot(self):

        time_str = time.strftime("%Y%m%d%H%M%S", time.localtime())
        file_path = self.work_path+'/screen_shot/'+f'{time_str}.png'
        self.logger.info(f'截屏，文件路径为<{file_path}>')
        self.driver.get_screenshot_as_file(file_path)

    def new_window(self, url):
        # 通过执行js，开启一个新的窗口
        js = f'window.open({url});'  
        self.excute_js(js)

    # def excute_js(self):
    #     js="return window.getComputedStyle(document.querySelector('i.tree-branch-head'),':after').getPropertyValue('content');"

    def move_to_ele(self, by, value):
        ele = self.locate(by, value)
        webdriver.ActionChains(self.driver).move_to_element(ele).perform()

if __name__ == '__main__':
    pass
